package com.runner.game;

import java.util.ArrayList;
import java.util.Iterator;

import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.runner.boost.Boost;
import com.runner.boost.CoinMultiplierBoost;
import com.runner.boost.MagnetBoost;
import com.runner.boost.ShieldBoost;
import com.runner.observer.Event;
import com.runner.observer.Observer;
import com.runner.observer.Subject;
import com.runner.player.April;
import com.runner.player.Geo;
import com.runner.player.Player;
import com.runner.player.Ringer;
import com.runner.track.Block;
import com.runner.track.CloudManager;
import com.runner.track.Track;
import com.runner.track.design.RingerTrack1;
import com.runner.track.design.RingerTrack2;
import com.runner.tutorial.AprilTutorial;
import com.runner.tutorial.GeoTutorial;
import com.runner.tutorial.RingerTutorial;
import com.runner.tutorial.Tutorial;
import com.runner.utility.Constants;
import com.runner.utility.Locator;

public class GameHandler implements Constants, Observer {
	private static final float DEATH_TIME = 1.0f;
	private Player player;
	private PLAYER_TYPE pType;
	private Track track;
	private TRACKTYPE tType;
	private ArrayList<Boost> boosts;
	private ArrayList<BOOST_TYPE> bTypes;
	private Subject subject;
	private int coins;
	private int distance;
	private float time;
	private boolean reset;
	private boolean death;
	private boolean win;
	private float deathTimer;
	private int topDistance;
	private Tutorial tutorial;
	private BitmapFont tutorialFont;
	private CloudManager cloudManager;

	public GameHandler(Subject subject, BitmapFont tutorialFont) {
		this.subject = subject;
		pType = PLAYER_TYPE.RINGER;
		boosts = new ArrayList<Boost>();
		bTypes = new ArrayList<BOOST_TYPE>();
		cloudManager=new CloudManager(55);
		subject.addObserver(this);
		coins = 0;
		topDistance = 0;
		reset = false;
		death = false;
		win = false;
		this.tutorialFont = tutorialFont;
	}

	public void loadLevel(PLAYER_TYPE pType, TRACKTYPE tType, ArrayList<BOOST_TYPE> bType) {
		reset = false;
		this.pType = pType;
		this.tType = tType;
		this.bTypes = bType;
		loadPlayer(pType);
		loadTrack(tType);
		
		cloudManager.loadClouds(pType);
		for (BOOST_TYPE b : bType) {
			loadBoost(b);
		}
		subject.notify(Event.EVENT_PLAYER_PICKED);
	}

	private void loadPlayer(PLAYER_TYPE type) {
		switch (type) {
		case RINGER:
			player = new Ringer(new Vector2(PLAYER_WIDTH * 4 / Player.SCALE * Locator.getxSpriteScale(), GROUND_HEIGHT * Locator.getySpriteScale()),
					subject);
			tutorial = new RingerTutorial(subject, tutorialFont);
			break;
		case APRIL:
			player = new April(new Vector2(PLAYER_WIDTH * 4 / Player.SCALE * Locator.getxSpriteScale(), GROUND_HEIGHT * Locator.getySpriteScale()),
					subject);
			tutorial = new AprilTutorial(subject, tutorialFont);
			break;
		case GEO:
			player = new Geo(new Vector2(PLAYER_WIDTH * 4 / Player.SCALE * Locator.getxSpriteScale(), GROUND_HEIGHT * Locator.getySpriteScale()),
					subject);
			tutorial = new GeoTutorial(subject, tutorialFont);
			break;
		default:
			break;
		}

	}

	/**
	 * Determine which track to use based on player and load its coin pattern.
	 * Also get top distance from that track
	 * 
	 * @param type
	 */
	private void loadTrack(TRACKTYPE type) {
		switch (pType) {
		case RINGER:
			switch (type) {
			case TRACK1:
				track = new Track(TRACK_RINGER_1_PATH);
				track.loadCoins(RingerTrack1.addCoins());
				topDistance = Locator.getAccomplishments().getDistanceRinger1();
				break;
			case TRACK2:
				track = new Track(TRACK_RINGER_2_PATH);
				track.loadCoins(RingerTrack2.addCoins());
				topDistance = Locator.getAccomplishments().getDistanceRinger2();
			default:
				break;
			}
			break;
		case APRIL:
			switch (type) {
			case TRACK1:
				track = new Track(TRACK_APRIL_1_PATH);
				topDistance = Locator.getAccomplishments().getDistanceApril1();
				break;
			case TRACK2:
				track = new Track(TRACK_APRIL_2_PATH);
				topDistance = Locator.getAccomplishments().getDistanceApril2();
			default:
				break;
			}
			break;
		case GEO:
			switch (type) {
			case TRACK1:
				track = new Track(TRACK_GEO_1_PATH);
				topDistance = Locator.getAccomplishments().getDistanceGeo1();
				break;
			case TRACK2:
				track = new Track(TRACK_GEO_2_PATH);
				topDistance = Locator.getAccomplishments().getDistanceGeo2();
			default:
				break;
			}
			break;
		}
	}

	private void loadBoost(BOOST_TYPE type) {
		switch (type) {
		case MAGNET:
			boosts.add(new MagnetBoost(player, track.getCoins()));
			break;
		case SHIELD:
			boosts.add(new ShieldBoost(player, subject));
			break;
		case COIN_MULT:
			boosts.add(new CoinMultiplierBoost(this, subject));
			break;
		default:
			break;
		}
	}

	public void render(SpriteBatch sb) {
		if (!reset) {
			cloudManager.render(sb);
			track.render(sb);
			player.render(sb);
			for (Boost b : boosts) {
				b.render(sb);
			}
			if (!death) {
				sb.end();
				tutorial.render();
				sb.begin();
			}
		}
	}

	public void update(float delta) {
		time += delta;
		cloudManager.update(delta);
		track.update(delta);
		player.update(delta);
		for (Iterator<Boost> i = boosts.iterator(); i.hasNext();) {
			Boost b = i.next();
			b.update(delta);
			if (!b.isExists()) {
				i.remove();
			}
		}

		// if the player has died, wait a couple seconds before showing results
		if (death) {
			player.getPhysics().getBody().setTransform(player.getPhysics().getBody().getPosition(), MathUtils.PI / 2);
			deathTimer += delta;
			if (deathTimer > DEATH_TIME) {
				death = false;
				if (!win) {
					subject.notify(Event.EVENT_SHOW_RESULTS);
				} else {
					subject.notify(Event.EVENT_SHOW_WIN_RESULTS);
				}
			}
		} else {
			tutorial.update(delta);
		}
	}

	public void reset() {
		cloudManager.reset();
		track.delete();
		player.delete();
		for (Boost b : boosts) {
			b.destroy();
		}
		boosts.clear();
		bTypes.clear();
		coins = 0;
		distance = 0;
		time = 0;
		reset = true;
		win = false;
		death = false;
	}

	public boolean touchDown(float x, float y, boolean tutorialPressed) {
		if (tutorialPressed) {
			tutorial.tap();
		} else {
			player.getInput().touchDown(x, y);
		}
		return false;
	}

	public boolean fling(float velocityX, float velocityY) {
		player.getInput().fling(velocityX, velocityY);
		return false;
	}

	public boolean tap(float x, float y) {
		player.getInput().tap(x, y);
		return false;
	}

	public void increaseCoins() {
		coins++;
	}

	@Override
	public void onNotify(Event event) {
		switch (event) {
		case EVENT_COIN:
			increaseCoins();
			break;
		case EVENT_WIN:
			deathAnimation(true);
			break;
		case EVENT_DEATH:
			deathAnimation(false);
			break;
		default:
			break;

		}
	}

	private void deathAnimation(boolean win) {
		calculateDistance();
		death = true;
		this.win = win;
		deathTimer = 0;
		// turn player sideways if s/he died
		if (!win) {
			player.setDirection(270);
		} else {
			notifyWin();
		}
		// add coins to total amount
		Locator.addCoins(coins);
		Locator.writeSave();
	}

	/** 
	 * broadcast that this track has been completed
	 */
	private void notifyWin() {
		switch (pType) {
		case RINGER:
			switch (tType) {
			case TRACK1:
				subject.notify(Event.EVENT_RINGER1_TRACK);
				break;
			case TRACK2:
				subject.notify(Event.EVENT_RINGER2_TRACK);
				if(player.hasShield()){
					subject.notify(Event.EVENT_RINGER2_TRACK_SHIELD);	
				}
			}
			break;
		case APRIL:
			switch (tType) {
			case TRACK1:
				subject.notify(Event.EVENT_APRIL1_TRACK);
				break;
			case TRACK2:
				subject.notify(Event.EVENT_APRIL2_TRACK);
				if(player.hasShield()){
					subject.notify(Event.EVENT_APRIL2_TRACK_SHIELD);	
				}
			}
			break;
		case GEO:
			switch (tType) {
			case TRACK1:
				subject.notify(Event.EVENT_GEO1_TRACK);
				break;
			case TRACK2:
				subject.notify(Event.EVENT_GEO2_TRACK);
				if(player.hasShield()){
					subject.notify(Event.EVENT_GEO2_TRACK_SHIELD);	
				}
			}
			break;
		}
		
	}

	private void calculateDistance() {
		float y = SCALE * Block.SPEED * Locator.getXGameSpeed();
		distance = (int) (time * (y));
		if (distance > topDistance) {
			topDistance = distance;
			subject.notify(Event.EVENT_NEW_TOP_DISTANCE);
			switch (pType) {
			case RINGER:
				switch (tType) {
				case TRACK1:
					Locator.getAccomplishments().setDistanceRinger1(topDistance);
					Locator.getDevice().submitLeaderboardScore(topDistance, LB_TYPE.RINGER1);
					break;
				case TRACK2:
					Locator.getAccomplishments().setDistanceRinger2(topDistance);
					Locator.getDevice().submitLeaderboardScore(topDistance, LB_TYPE.RINGER2);
				default:
					break;
				}
				break;
			case APRIL:
				switch (tType) {
				case TRACK1:
					Locator.getAccomplishments().setDistanceApril1(topDistance);
					Locator.getDevice().submitLeaderboardScore(topDistance, LB_TYPE.APRIL1);
					break;
				case TRACK2:
					Locator.getAccomplishments().setDistanceApril2(topDistance);
					Locator.getDevice().submitLeaderboardScore(topDistance, LB_TYPE.APRIL2);
				default:
					break;
				}
				break;
			case GEO:
				switch (tType) {
				case TRACK1:
					Locator.getAccomplishments().setDistanceGeo1(topDistance);
					Locator.getDevice().submitLeaderboardScore(topDistance, LB_TYPE.GEO1);
					break;
				case TRACK2:
					Locator.getAccomplishments().setDistanceGeo2(topDistance);
					Locator.getDevice().submitLeaderboardScore(topDistance, LB_TYPE.GEO2);
				default:
					break;
				}
				break;
			}
		}

	}

	public int getDistance() {
		return distance;
	}

	public int getCoins() {
		return coins;
	}

	public void addCoins(int coins) {
		Locator.addCoins(coins);
		this.coins += coins;
		Locator.writeSave();
	}

	public void loadSameLevel() {
		loadLevel(pType, tType, bTypes);

	}

	public PLAYER_TYPE getpType() {
		return pType;
	}

	public void setpType(PLAYER_TYPE pType) {
		this.pType = pType;
	}

	public int getTopDistance() {
		return topDistance;
	}

	public void setTopDistance(int topDistance) {
		this.topDistance = topDistance;
	}

}
